/**
 * \brief 使用ffmpeg将yuv编码成h264
 * \author 将狼才鲸
 * \date 2022-04-02
 * \note 第一次编译运行会报错！注意事项：
 * 先点击“Debug”运行一次，自动创建出可执行文件输出文件夹
 * 此时“3 应用程序输出”窗口最后一行只是显示了红色的异常信息“程序异常结束。”，
 * 则表示没有把../ffmpeg-4.3.1-win64-shared/bin/下的*.dll动态库拷贝到
 * ../build-05_ffmpeg_yuv2h264-Desktop_Qt_6_2_3_MinGW_64_bit-Debug/debug/
 * 下的可执行文件目录，拷贝完成后再点击“Debug”运行，软件才能正常工作。
 *
 * \note libavcodec默认通过id查找编解码器的方式都只能找到软解码器，通过名字
 *       名字查找的方式才能找到硬解码器，可以通过ffmpeg -codecs命令查看硬件解码器的名字
 *
 * \details 参考网址：
 *          [FFmpeg —— 13.示例程序（七）：视频编码器（YUV编码为H265）](https://www.dandelioncloud.cn/article/details/1441257592063643649)
 */

#include "mainwindow.h"
#include <QApplication>

extern "C" int yuv2hevc(int argc, char* argv[]);

int main(int argc, char *argv[])
{
//    QApplication a(argc, argv);
//    MainWindow w;
//    w.show();
//    return a.exec();
    yuv2hevc(0, NULL);

    return 0;
}

/**
 * \brief YUV像素数据编码为视频码流，方法一
 * \details 使用AVFormat和AVCodec
 * \note h264测试已通过，同样的代码只改下输出文件名hevc也测试通过
 */
#if 1
#include <stdio.h>
#define __STDC_CONSTANT_MACROS
extern "C"
{
#include "libavdevice/avdevice.h"
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#include "libavutil/opt.h"
};

// 编码最后一帧
int flush_encoder(AVFormatContext *fmtCtx, unsigned int streamIndex)
{
    int ret;
    int got_frame;
    AVPacket enc_pkt;

    if (!(fmtCtx->streams[streamIndex]->codec->codec->capabilities & AV_CODEC_CAP_DELAY))
    {
        return 0;
    }
    while (1)
    {
        enc_pkt.data = NULL;
        enc_pkt.size = 0;
        av_init_packet(&enc_pkt);
        ret = avcodec_encode_video2(fmtCtx->streams[streamIndex]->codec, &enc_pkt,
              NULL, &got_frame);
        av_frame_free(NULL);
        if (ret < 0)
            break;
        if (!got_frame) {
            ret=0;
            break;
        }
        printf("Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n", enc_pkt.size);
        /* mux encoded frame */
        ret = av_write_frame(fmtCtx, &enc_pkt);
        if (ret < 0)
            break;
    }

    return ret;
}

int yuv2hevc(int argc, char *argv[])
{
    AVFormatContext *pFormatCtx = NULL;
    AVCodecContext *pCodecCtx = NULL;
    AVCodec *pCodec = NULL;
    AVOutputFormat *pOutFmt = NULL;
    const char *inFilename =
    "../../004_MultiMedia_VideoAudio/003_yuv_video_play/003_yuv_video_play.yuv";
    const char *outFilename = "test.h264";
    //const char *outFilename = "test.hevc";

    // 1.注册所有输入输出设备
    avdevice_register_all();
    // 2.初始化封装格式上下文
    // 方法一：
//    pFormatCtx = avformat_alloc_context();
//    pOutFmt = av_guess_format(NULL, outFilename, NULL);
//    pFormatCtx->oformat = pOutFmt;
    // 方法二：
    avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, outFilename);
    pOutFmt = pFormatCtx->oformat;

    // 3.打开输出文件
    if (avio_open(&pFormatCtx->pb, outFilename, AVIO_FLAG_WRITE) < 0)
    {
        printf("can't open output file\n");
        return -1;
    }

    // 4.创建输出码流
    AVStream *pOutStream = avformat_new_stream(pFormatCtx, NULL);
    if (!pOutStream)
    {
        printf("can't allocate new stream\n");
        return -1;
    }
    // 5.查找视频编码器
    // 获取编码器上下文
//    avcodec_parameters_to_context(pCodecCtx, pOutStream->codecpar);
    pCodecCtx = pOutStream->codec;
    // 设置编码器上下文参数
    pCodecCtx->codec_id = pOutFmt->video_codec;
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
    // 视频的宽度和高度：确保等于输入文件的宽度和高度
    pCodecCtx->width = 128;
    pCodecCtx->height = 96;
    // 设置帧率25fps
    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = 25;
    // 设置码率
    pCodecCtx->bit_rate = 400000;
    // 设置GOP
    pCodecCtx->gop_size = 250;
    // 设置量化参数
    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;
//    pCodecCtx->max_b_frames = 0;
    // 6.查找编码器
    pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
    if (!pCodec)
    {
        printf("can't find encoder\n");
        return -1;
    }
    printf("pCodec.name = %s\n", pCodec->name);

    // 若是H264编码器，要设置一些参数
    AVDictionary *param = NULL;
    if (pCodecCtx->codec_id == AV_CODEC_ID_H264)
    {
        av_dict_set(&param, "preset", "slow", 0);
        av_dict_set(&param, "tune", "zerolatency", 0);
    }
    // H.265
    if(pCodecCtx->codec_id == AV_CODEC_ID_H265){
//        av_dict_set(&param, "x265-params", "qp=20", 0);
        av_dict_set(&param, "preset", "ultrafast", 0);
        av_dict_set(&param, "tune", "zero-latency", 0);
    }

    // 7.打开编码器
    if (avcodec_open2(pCodecCtx, pCodec, &param) < 0)
    {
        printf("can't open encoder\n");
        return -1;
    }

    // 8.写入头文件信息
    avformat_write_header(pFormatCtx, NULL);

    // 9.循环编码YUV文件为H264
    // (1)开辟缓冲区
    int bufSize = av_image_get_buffer_size(pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, 1);
    int ySize = pCodecCtx->width * pCodecCtx->height;
    uint8_t *outBuffer = (uint8_t *)av_malloc(bufSize);
    FILE *inFile = fopen(inFilename, "rb");
    if (!inFile)
    {
        printf("can't find input file\n");
        return -1;
    }

    // (2) 内容空间填充
    AVFrame *pFrame = av_frame_alloc();
    // 设置真的格式、宽度和高度，否则会出现
    // AVFrame.format is not set
    // AVFrame.width or height is not set
    pFrame->format = pCodecCtx->pix_fmt;
    pFrame->width = pCodecCtx->width;
    pFrame->height = pCodecCtx->height;
    av_image_fill_arrays(pFrame->data,
            pFrame->linesize,
            outBuffer,
            pCodecCtx->pix_fmt,
            pCodecCtx->width,
            pCodecCtx->height,
            1);

    // (3)开辟packet
    AVPacket *pPacket = (AVPacket *)av_malloc(bufSize);
    int i = 0, frameIndex = 0;

    // (4)循环编码
    while(1)
    {
        // 从YUV文件里面读取缓冲区
        // 读取大小：ySize * 3 / 2
        if (fread(outBuffer, 1, ySize * 3 / 2, inFile) <= 0)
        {
            printf("finished to read data\n");
            break;
        }
        else if (feof(inFile))
        {
            break;
        }

        // 将缓冲区数据转换成AVFrame类型
        pFrame->data[0] = outBuffer;            //Y值
        pFrame->data[1] = outBuffer + ySize;    //U值
        pFrame->data[2] = outBuffer + ySize * 5 / 4;    //V值
        pFrame->pts = i * (pOutStream->time_base.den) / (pOutStream->time_base.num * 25);

        // 10.视频编码处理
        //（1）发送一帧视频像素数据
        if (avcodec_send_frame(pCodecCtx, pFrame) < 0)
        {
            printf("failed to encoder\n");
            return -1;
        }

        // (2)接收一帧视频压缩数据格式（像素数据编码而来）
        if (avcodec_receive_packet(pCodecCtx, pPacket) >= 0)
        {
            // 编码成功
            // 11.将视频写入到输出文件
            pPacket->stream_index = pOutStream->index;
            if (av_write_frame(pFormatCtx, pPacket) < 0)
            {
                printf("failed to write frame\n");
                return -1;
            }
            printf("succeed to write frame: %d\tsize:%d\n", frameIndex++, pPacket->size);
        }
        av_packet_unref(pPacket);
    }

    // 写入剩余帧数据->可能没有
    flush_encoder(pFormatCtx, 0);
    // 写入文件尾部信息
    av_write_trailer(pFormatCtx);

    // 释放内存
    avcodec_close(pCodecCtx);
    av_free(pFrame);
    av_free(outBuffer);
    av_packet_free(&pPacket);
    avio_close(pFormatCtx->pb);
    avformat_free_context(pFormatCtx);
    fclose(inFile);

    return 0;
}
#endif

/**
 * \brief YUV像素数据编码为视频码流，方法二
 * \details 只使用AVCodec，不使用AVFormat
 * \note 测试通过
 */
#if 0
extern "C"
{
#define __STDC_CONSTANT_MACROS
#include <stdio.h>
#include "libavcodec/avcodec.h"
#include "libswscale/swscale.h"
#include "libavutil/opt.h"
#include "libavutil/imgutils.h"
};

// test different codec
#define TEST_H264  1
#define TEST_HEVC  0 // H265

int yuv2hevc(int argc, char* argv[])
{
    AVCodec *pCodec;
    AVCodecContext *pCodecCtx = NULL;
    AVFrame *src_frame, *dst_frame;// 一帧未编码的像素数据格式转换输入
    AVPacket pkt; // 存储一帧编码后的包
    struct SwsContext *img_convert_ctx;
    int i = 0, ret, got_output;
    FILE *fp_in;
    FILE *fp_out;
    int y_size;
    int framecnt = 0;

    char filename_in[] =
    "../../004_MultiMedia_VideoAudio/003_yuv_video_play/003_yuv_video_play.yuv";
#if TEST_HEVC
    AVCodecID codec_id = AV_CODEC_ID_HEVC;
    char filename_out[] = "test.hevc";
#else
    AVCodecID codec_id = AV_CODEC_ID_H264;
    char filename_out[] = "test.h264";
#endif

    int src_w = 128, src_h = 96;
    int dst_w = 128, dst_h = 96;
    int framenum = 100;

    avcodec_register_all(); // 注册所有编解码器相关的组件。av_register_all()：注册所有的编解码器，复用/解复用器等等组件

    pCodec = avcodec_find_encoder(codec_id); // 查找ID值为AV_CODEC_ID_H264的AAC编码器
    if (!pCodec) {
        printf("Codec not found\n");
        return -1;
    }
    pCodecCtx = avcodec_alloc_context3(pCodec); // 为AVCodecContext分配内存 创建AVCodecContext结构体。AVCodecContext中很多的参数是编码的时候使用
    if (!pCodecCtx) {
        printf("Could not allocate video codec context\n");
        return -1;
    }
    pCodecCtx->bit_rate = 400000; // 平均比特率 目标的码率，即采样的码率；显然，采样码率越大，视频大小越大
    pCodecCtx->width = dst_w; // 如果是视频的话，代表宽和高 编码目标的视频帧大小，以像素为单位
    pCodecCtx->height = dst_h;
    // 帧率的基本单位，我们用分数来表示，
    // 用分数来表示的原因是，有很多视频的帧率是带小数的 eg：NTSC 使用的帧率是29.97
    pCodecCtx->time_base.num = 1; // 根据该参数，可以把PTS转化为实际的时间（单位为秒s）
    pCodecCtx->time_base.den = 25;
    pCodecCtx->gop_size = 10; // 一组图片中的图片数
    // 两个非B帧之间允许出现多少个B帧数
    // 设置0表示不使用B帧
    // b 帧越多，图片越小
    pCodecCtx->max_b_frames = 1;
    pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P; // 像素格式 也就是说采用什么样的色彩空间来表明一个像素点
    // 设置帧率
    pCodecCtx->framerate = {10, 1};
    // 量化参数设置（影响视频清晰度） 越小越清晰
    pCodecCtx->qmax = 51;
    pCodecCtx->qmin = 10;

    if (codec_id == AV_CODEC_ID_H264)
        av_opt_set(pCodecCtx->priv_data, "preset", "slow", 0);

    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) { // 打开编码器。
        printf("Could not open codec\n");
        return -1;
    }

    src_frame = av_frame_alloc(); // 分配AVFrame AVFrame结构体一般用于存储原始数据（即非压缩数据，例如对视频来说是YUV，RGB，对音频来说是PCM
    if (!src_frame) {
        printf("Could not allocate video frame\n");
        return -1;
    }
    src_frame->format = AV_PIX_FMT_NV12;//pCodecCtx->pix_fmt; // 帧的格式
    src_frame->width  = src_w;
    src_frame->height = src_h; // 视频帧宽和高（1920x1080,1280x720...）

    dst_frame = av_frame_alloc();
    dst_frame->format = AV_PIX_FMT_YUV420P; // 帧的格式
    dst_frame->width  = dst_w;
    dst_frame->height = dst_h;

    // 初始化SwsContext
    img_convert_ctx = sws_getContext(src_frame->width, src_frame->height, \
                                     pCodecCtx->pix_fmt, dst_frame->width, dst_frame->height, \
                                     pCodecCtx->pix_fmt, 0, nullptr, nullptr, nullptr);

//    int dst_bytes_num = avpicture_get_size(pCodecCtx->pix_fmt, dst_frame->width, dst_frame->height);//计算这个格式的图片，需要多少字节来存储
//    uint8_t* dst_buff = (uint8_t *)av_malloc(dst_bytes_num * sizeof(uint8_t));//申请空间来存放图片数据。包含源数据和目标数据
    //    前面的av_frame_alloc函数，只是为这个AVFrame结构体分配了内存，
    //    而该类型的指针指向的内存还没分配。这里把av_malloc得到的内存和AVFrame关联起来。
    //    当然，其还会设置AVFrame的其他成员
//    avpicture_fill((AVPicture *)dst_frame, dst_buff, pCodecCtx->pix_fmt, dst_frame->width, dst_frame->height);//ffmpeg4.2.2中就已经被抛弃了, 取而代之的是av_image_fill_arrays()
//以上注释的代码也能实现 av_image_alloc

    av_image_alloc(dst_frame->data, dst_frame->linesize, dst_frame->width, dst_frame->height,
                         pCodecCtx->pix_fmt, 16);

    ret = av_image_alloc(src_frame->data, src_frame->linesize, src_frame->width, src_frame->height,
                         pCodecCtx->pix_fmt, 16);
    // 按照指定的宽、高、像素格式来分析图像内存
    //    pointers[4]：保存图像通道的地址。如果是RGB，则前三个指针分别指向R,G,B的内存地址。第四个指针保留不用
    //    linesizes[4]：保存图像每个通道的内存对齐的步长，即一行的对齐内存的宽度，此值大小等于图像宽度。
    //    w:              要申请内存的图像宽度。
    //    h:              要申请内存的图像高度。
    //    pix_fmt:        要申请内存的图像的像素格式。
    //    align:          用于内存对齐的值。
    //    返回值：所申请的内存空间的总大小。如果是负值，表示申请失败。
    if (ret < 0) {
        printf("Could not allocate raw picture buffer\n");
        return -1;
    }

    // Input raw data
    fp_in = fopen(filename_in, "rb"); // 使用给定的模式 mode 打开 filename 所指向的文件。 rb+读写打开一个二进制文件，允许读数据。
    if (!fp_in) {
        printf("Could not open %s\n", filename_in);
        return -1;
    }

    // Output bitstream
    fp_out = fopen(filename_out, "wb"); // wb 只写打开或新建一个二进制文件；只允许写数据。
    if (!fp_out) {
        printf("Could not open %s\n", filename_out);
        return -1;
    }

    y_size = src_frame->width * src_frame->height;

    // Encode
    for (i = 0; i < framenum; i++) {
        av_init_packet(&pkt); // 设置默认值
        pkt.data = NULL; // packet data will be allocated by the encoder
        pkt.size = 0;    // 从给定流 fp_in 读取数据到 src_frame->data[0] 所指向的数组中

        // Read raw YUV data
        if (fread(src_frame->data[0],1,y_size,fp_in) <= 0 ||		// Y
                fread(src_frame->data[1],1,y_size/4,fp_in) <= 0 ||	// U
                fread(src_frame->data[2],1,y_size/4,fp_in) <= 0) {	// V
            return -1;
        } else if (feof(fp_in)) { // 测试给定流 fp_in 的文件结束标识符。
            break;
        }

        sws_scale(img_convert_ctx, (uint8_t const * const *)src_frame->data,
                  src_frame->linesize, 0, src_frame->height, dst_frame->data, dst_frame->linesize);

        dst_frame->pts = i * 12800 / 25;
        /* encode the image */
        ret = avcodec_encode_video2(pCodecCtx, &pkt, dst_frame, &got_output); // 编码一帧数据。要传转换完的 dst_frame

        if (ret < 0) {
            printf("Error encoding frame\n");
            return -1;
        }
        if (got_output) {
            printf ("Succeed to encode frame: %5d\tsize:%5d\n", framecnt, pkt.size);
            framecnt++;
            fwrite(pkt.data, 1, pkt.size, fp_out); // 把 pkt.data 所指向的数组中的数据写入到给定流 fp_out 中
            av_free_packet(&pkt); // 清空pkt中data以及buf的内容，并没有把pkt的指针清空
        }
    }

    // Flush Encoder
    for (got_output = 1; got_output; i++) {
        ret = avcodec_encode_video2(pCodecCtx, &pkt, NULL, &got_output);
        if (ret < 0) {
            printf("Error encoding frame\n");
            return -1;
        }
        if (got_output) {
            printf("Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n",pkt.size);
            fwrite(pkt.data, 1, pkt.size, fp_out);
            av_free_packet(&pkt);
        }
    }

    fclose(fp_out); // 关闭流 fp_out。刷新所有的缓冲区。
    avcodec_close(pCodecCtx); // 关闭编码器
    av_free(pCodecCtx); // 释放已分配av_malloc（） AVCodecContext
    av_freep(&src_frame->data[0]); // 释放并清理指针
    av_frame_free(&src_frame); // 释放AVFrame
    av_frame_free(&dst_frame); // 释放AVFrame
    sws_freeContext(img_convert_ctx);

    return 0;
}
#endif
